package edu.umb.cs.antmanager.manager;

import static edu.umb.cs.antmanager.common.Constants.*;

import java.io.*;
import java.util.Scanner;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.xml.sax.*;
import org.xml.sax.helpers.*;

import edu.umb.cs.antmanager.domain.FailPatternType;
import edu.umb.cs.antmanager.domain.WarnPatternType;

/**
 * Using SAX to build the log, reduce the memory usage for large log file
 * 
 * @author 2009CS682-3UMB_AntManager
 * 
 */
public class SaxLogBuilder {

	private static SaxLogBuilder logBuilder;
	private static final String LOG_FILENAME = "log";
	private static final String SUMMARY_FILENAME = "summary";
	private static final String LOG_ROOT_NAME = "testResults";
	private static final String CDATA_ELEMENT = "message";
	private static final String TARGET_ELEMENT = "target";

	/**
	 * Singleton factory method get the current <tt>SaxLogBuilder</tt>, if not
	 * exist create a new one.
	 * 
	 * @return current <tt>SaxLogBuider</tt>
	 */
	public static synchronized SaxLogBuilder getLogBuilder() {
		if (logBuilder == null)
			logBuilder = new SaxLogBuilder();
		return logBuilder;
	}

	/**
	 * initialize an empty log document and summary document
	 */
	private SaxLogBuilder() {
		if (!Boolean.getBoolean(ANTMANAGER_TEST_CONTINUE))
			try {
				BufferedWriter writer = new BufferedWriter(
						new OutputStreamWriter(new FileOutputStream(
								ANT_MANAGER_HOME + LOG_FILENAME + ".xml")));
				writeHeader(writer);
				writeElementStart(writer, LOG_ROOT_NAME, null);
				writer.newLine();
				writeElementEnd(writer, LOG_ROOT_NAME);
				writer.close();

				writer = new BufferedWriter(new OutputStreamWriter(
						new FileOutputStream(ANT_MANAGER_HOME
								+ SUMMARY_FILENAME + ".xml")));
				writeHeader(writer);
				AttributesImpl summaryAttr = new AttributesImpl();
				summaryAttr.addAttribute("", "buildID", "", "String", "");
				summaryAttr.addAttribute("", "date", "", "String", "");
				summaryAttr.addAttribute("", "logfile", "", "String",
						LOG_FILENAME + ".html");
				summaryAttr.addAttribute("", "type", "", "String", "test");
				summaryAttr.addAttribute("", "update", "", "String", "yes");
				summaryAttr.addAttribute("", "version", "", "String", "1.0");
				writeElementStart(writer, "Summary", summaryAttr);
				writer.newLine();
				writeElementEnd(writer, "Summary");
				writer.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
	}

	/**
	 * Insert a test result into log file and summary file
	 * 
	 * @param xmlStream
	 *            The <code>InputStream<code> contains the test result
	 * @param info
	 *            Java bean contains detail information about the test
	 */
	public void addTestResult(InputStream xmlStream, TestInfo info) {
		try {
			// Temporary file for the log inserting operation
			File tmp = new File(ANT_MANAGER_HOME + LOG_FILENAME + ".tmp");
			File log = new File(ANT_MANAGER_HOME + LOG_FILENAME + ".xml");
			BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
					new FileOutputStream(tmp)));
			XMLReader xr = XMLReaderFactory.createXMLReader();
			// create and set the ContentHandler
			xr.setContentHandler(new ResultLogHandler(writer, xmlStream, info));
			// parse the log file using the above ContentHandler
			xr.parse(new InputSource(new BufferedReader(new FileReader(log))));
			writer.close();
			xmlStream.close();
			log.delete();
			tmp.renameTo(log);
			tmp.delete();

			// Transferring XML to HTML
			TransformerFactory xformFactory = TransformerFactory.newInstance();
			Source input = new StreamSource(log);
			Result htmlOutput = new StreamResult(new File(ANT_MANAGER_HOME
					+ LOG_FILENAME + ".html"));
			Source styleSheet = new StreamSource(new File(ANT_MANAGER_HOME
					+ "buildLog.xsl"));
			Transformer htmlLogTransformer = xformFactory
					.newTransformer(styleSheet);
			htmlLogTransformer.transform(input, htmlOutput);
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Writer XML header using the provided Writer
	 * 
	 * @param writer
	 *            Stream to write to
	 */
	private void writeHeader(BufferedWriter writer) {
		try {
			writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
			writer.newLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Write an Element start.
	 * 
	 * @param writer
	 *            Stream to write to
	 * @param localName
	 *            Local name of the element
	 * @param attr
	 *            Attributes of the element
	 */
	private void writeElementStart(BufferedWriter writer, String localName,
			Attributes attr) {
		try {
			writer.write("<" + localName);
			if (attr != null)
				for (int i = 0; i < attr.getLength(); i++) {
					writer.write(" " + attr.getLocalName(i) + "=\""
							+ attr.getValue(i) + "\"");
				}
			writer.write(">");
			// if(localName.equals(LOG_ROOT_NAME)) writer.newLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Write Element end.
	 * 
	 * @param writer
	 *            Stream to write to
	 * @param localName
	 *            Local name of the element
	 */
	private void writeElementEnd(BufferedWriter writer, String localName) {
		try {
			writer.write("</" + localName + ">");
			// writer.newLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Writer character data.
	 * 
	 * @param writer
	 *            Stream to write to
	 * @param hasCDATA
	 *            Whether it's CDATA
	 * @param ch
	 *            Character array
	 * @param start
	 *            Start offset
	 * @param length
	 *            The length of the characters
	 */
	private void writeChars(BufferedWriter writer, boolean hasCDATA, char[] ch,
			int start, int length) {
		try {
			if (hasCDATA)
				writer.write("<![CDATA[");
			writer.write(ch, start, length);
			if (hasCDATA)
				writer.write("]]>");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Content handler for inserting new test result to the log file
	 * 
	 * @author 2009CS682-3UMB_AntManager
	 * 
	 */
	private class ResultLogHandler extends DefaultHandler {

		private final BufferedWriter writer;
		private final InputStream buildLog;
		private boolean hasCDATA = false;
		private TestInfo info;

		/**
		 * 
		 * @param writer
		 *            The stream to write the final result
		 * @param is
		 *            The stream of test result to insert
		 * @param info
		 *            Test information
		 */
		public ResultLogHandler(BufferedWriter writer, InputStream is,
				TestInfo info) {
			this.writer = writer;
			this.buildLog = is;
			this.info = info;
		}

		@Override
		public void startDocument() {
			writeHeader(writer);
		}

		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) {
			writeElementStart(writer, localName, attributes);
			if (localName.equals(CDATA_ELEMENT))
				hasCDATA = true;
		}

		@Override
		public void endElement(String uri, String localName, String qName) {
			if (localName.equals(LOG_ROOT_NAME)) {
				// we have reach the end the document, this is the place to
				// insert the new test result
				try {
					// parse the test result, which is XML
					XMLReader xr = XMLReaderFactory.createXMLReader();
					xr.setContentHandler(new BuildLogHandler(writer, info));
					xr.parse(new InputSource(buildLog));
				} catch (SAXException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}

			}
			writeElementEnd(writer, localName);
			if (localName.equals(CDATA_ELEMENT))
				hasCDATA = false;
		}

		@Override
		public void characters(char[] ch, int start, int length) {
			writeChars(writer, hasCDATA, ch, start, length);
		}

	}

	/**
	 * ContentHandler for parsing single test result and insert it into the log
	 * file, also extract summary information and insert it into summary file
	 * 
	 * @author 2009CS682-3UMB_AntManager
	 * 
	 */
	private class BuildLogHandler extends DefaultHandler {
		private final BufferedWriter writer;
		private boolean hasCDATA = false;
		private TestInfo info;
		private String summaryText;

		/**
		 * 
		 * @param writer
		 *            Stream to write the result
		 * @param info
		 *            Test information
		 */
		public BuildLogHandler(BufferedWriter writer, TestInfo info) {
			this.writer = writer;
			this.info = info;
		}

		@Override
		public void endDocument() {
			// reach the end of test stream, insert summary
			File sumTmp = new File(ANT_MANAGER_HOME + SUMMARY_FILENAME + ".tmp");
			File sumFile = new File(ANT_MANAGER_HOME + SUMMARY_FILENAME
					+ ".xml");
			try {
				BufferedWriter sumWriter = new BufferedWriter(
						new OutputStreamWriter(new FileOutputStream(sumTmp)));
				Scanner sumReader = new Scanner(sumFile);
				while (sumReader.hasNextLine()) {
					String line = sumReader.nextLine();
					// find the insert point in the summary file
					if (line.startsWith("</Summary>")) {
						// generate the summary
						String result = "PASS";
						if (info.isWarn())
							result = "WARN";
						if (info.isError())
							result = "FAIL";
						AttributesImpl attr = new AttributesImpl();
						attr.addAttribute("", "anchor", "", "String", info
								.getTest().getName());
						attr.addAttribute("", "startTime", "", "String", info
								.getStartTime().toString());
						attr.addAttribute("", "endTime", "", "String", info
								.getEndTime().toString());
						attr.addAttribute("", "priority", "", "String", "1");
						attr.addAttribute("", "resource", "", "String", info
								.getAgentName());
						attr.addAttribute("", "result", "", "String", result);
						attr.addAttribute("", "title", "", "String", info
								.getTest().getName());
						attr.addAttribute("", "visible", "", "String", "true");
						writeElementStart(sumWriter, "phase", attr);
						sumWriter.newLine();
						sumWriter.write("<![CDATA[");
						sumWriter.newLine();
						sumWriter.write("<pre>");
						sumWriter.newLine();
						sumWriter.write(summaryText);
						sumWriter.write(info.getTest().getSummary().getEnd());
						sumWriter.newLine();
						sumWriter.write("<pre>");
						sumWriter.newLine();
						sumWriter.write("]]>");
						sumWriter.newLine();
						writeElementEnd(sumWriter, "phase");
						sumWriter.newLine();
					}
					sumWriter.write(line);
					sumWriter.newLine();
				}
				sumWriter.close();
				sumReader.close();
				sumFile.delete();
				sumTmp.renameTo(sumFile);
				sumTmp.delete();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) {
			if (localName.equals(TARGET_ELEMENT)) {
				// Change target name to test name
				AttributesImpl attr = new AttributesImpl(attributes);
				attr.setValue(attr.getIndex(uri, "name"), info.getTest()
						.getName());
				writeElementStart(writer, localName, attr);
			} else
				writeElementStart(writer, localName, attributes);
			if (localName.equals(CDATA_ELEMENT))
				hasCDATA = true;
		}

		@Override
		public void endElement(String uri, String localName, String qName) {
			writeElementEnd(writer, localName);
			if (localName.equals(CDATA_ELEMENT))
				hasCDATA = false;
		}

		@Override
		public void characters(char[] ch, int start, int length) {
			if (hasCDATA) {
				// Try to get the summary text
				String text = new String(ch, start, length);
				// Check if the test has Error
				for (FailPatternType fpt : info.getTest().getFailPatternArray()) {
					if (text.contains(fpt.getValue()))
						info.setError(true);
				}
				// Check if the test has warnning
				for (WarnPatternType wpt : info.getTest().getWarnPatternArray()) {
					if (text.contains(wpt.getText()))
						info.setWarn(true);
				}
				// save the summary text
				if (text.contains(info.getTest().getSummary().getStart())) {
					int begin = text.indexOf(info.getTest().getSummary()
							.getStart());
					int end = text
							.indexOf(info.getTest().getSummary().getEnd());
					summaryText = text.substring(begin, end);
				}
			}
			writeChars(writer, hasCDATA, ch, start, length);
		}

	}
}
